package name.shamansir.tadah.data.parser.quicktask;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.text.format.Time;

import name.shamansir.tadah.data.TodoItem;
import name.shamansir.tadah.data.Tag.Type;
import name.shamansir.tadah.data.parser.CanParseQuickTask;
import name.shamansir.tadah.data.scheduling.Occurrence;
import name.shamansir.tadah.data.scheduling.OverlapTest;
import name.shamansir.tadah.data.scheduling.SchedulingRule;
import name.shamansir.tadah.data.scheduling.rules.OccursSingleTime;
import name.shamansir.tadah.data.scheduling.rules.RepeatingRule;
import name.shamansir.tadah.data.scheduling.rules.RepeatsByMinutes;
import name.shamansir.tadah.data.scheduling.rules.RepeatsByWeekdays;
import name.shamansir.tadah.data.scheduling.rules.RepeatsDatesInYear;
import name.shamansir.tadah.data.scheduling.rules.RepeatsDaysInMonth;
import name.shamansir.tadah.data.scheduling.rules.RepeatsEveryDay;
import name.shamansir.tadah.data.scheduling.rules.RepeatsMonthsInYear;

/**
 * <dl>
 * <dt>Project:</dt> <dd>ta-dah</dd>
 * <dt>Package:</dt> <dd>name.shamansir.tadah.util</dd>
 * </dl>
 *
 * <code>EnParser</code>
 *
 * <p>Description</p>
 *
 * @author Ulric Wilfred <shaman.sir@gmail.com>
 * @date Feb 23, 2011 8:51:16 PM 
 *
 */
public class EnParser implements CanParseQuickTask {
    
    @Override
    public Locale getLocale() { return Locale.ENGLISH; }
    
    private static final String __NUMBER__ = 
        "one|two|three|four|five|six|seven|eight|nine|ten";
    private static final String __MONTH__ =
        "jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?ember)?|dec(?ember)?";
    private static final String __WEEKDAY__ =  
        "mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?";
    private static final String __PERIOD__ =
        "days?|weeks?|months?|mnths?|years?|yrs?|hours?|hrs?|h|minutes?|mnts?";
    private static final String __OCCUR__ =
        "day|week|month|mnth|year|yr|hour|hr?|minute|mnt";
    
    protected enum PatternType {
        
        // my first project, my second project: do fun stuff
        PROJECTS_LIST("^\\s*([\\w\\s-]*)(?:\\s*,\\s*([\\w\\s-]*))*\\s*:"),
        // fun stuff // fun, stuff, happy (aligned to end)
        TAGS_LIST("//\\s*([\\w\\s-]*)(?:\\s*,\\s*([\\w\\s-]*))*\\s*$"),
        // meeting at molly's house, mcdonalds (aligned to end)
        LOCATIONS_LIST("(?:at|@)\\s*([\\w\\s-]*)(?:\\s*,\\s*([\\w\\s-]*))*\\s*$"),        
        // project meeting today 
        TODAY("(today|tdy)"),
        // 'twas project meeting yesterday
        YESTERDAY("(yesterday|ys?trd)"),
        // project meeting tomorrow
        TOMORROW("(tomorrow|twrm|tmrw)"),
        // meeting yesterday+2, meeting +5 
        PLUS_MINUS("([+-])\\s*(\\d+)"),
        // meeting two months fwd, meeting week back
        BACK_FWD("("+__NUMBER__+"|\\d+)\\s*("+__PERIOD__+")\\s+(ba?ck|before|b4|ago|fwd|forward|forth)"),
        // meeting in two hrs, work must be done in 3 weeks  
        AFTER("(?:after|in)\\s+("+__NUMBER__+"|\\d+)\\s*("+__PERIOD__+")"),
        // vacation for two weeks, 5 months of making a great thing 
        DURATION("(?:for\\s+)?("+__NUMBER__+"|\\d+)\\s*("+__PERIOD__+")(?:\\s+of)?"),
        // every year 25 feb birthday
        EVERY_PERIOD("eve?ry\\s*("+ __OCCUR__+")"),
        // every two years, evry 2 weeks, every 15 hours
        EVERY_PERIOD_AMOUNT("eve?ry\\s*("+__NUMBER__+"|\\d+)\\s*("+__PERIOD__+")"),
        // send sms every 15, 12, 25 
        EVERY_DATE("eve?ry\\s+([0-3]\\d)(?:\\s*,\\s*([0-3]\\d))*"),
        // send sms at 15h, 12h, 22h // will match TIME
        //EVERY_HOUR("eve?ry\\s+([0-3]\\dh)(?:\\s*,\\s*([0-3]\\dh))*"),
        // coffee every thursday, fri, mon 
        EVERY_WEEKDAY("eve?ry\\s+("+__WEEKDAY__+")(?:\\s*,\\s*(" + __WEEKDAY__ +"))*"),
        // spring is every march, april, may  
        EVERY_MONTH("eve?ry\\s+("+__MONTH__+")(?:\\s*,\\s*(" + __MONTH__ +"))*"),
        // meeting next thursday, anniversary next month 
        NEXT("(?:next|nxt)\\s+("+ __OCCUR__+")"),
        // just period, this week, this month
        PERIOD("(?:this|all)?\\s+("+ __OCCUR__+")"),
        // machine works from 2th june, sports from tuesday to friday, meeting from 5h to 7h, sports from may every friday 
        FROM("((?:starting\\s+)?from"),
        // festival from 12 jun to 15 aug
        TO("to|till"),
        // meeting 15 jan
        MONTH("("+__MONTH__+")"),
        // meeting thursday
        WEEKDAY("("+__WEEKDAY__+")"),
        // 28 Feb, 02.28, 30 march 1989, 02 may 2008, 3 Feb, 5th mar 95, 03.15.11, 04-13-2005
        DATE_LONG("([0-3]?\\d)(?:th|st|nd|rd)?\\s*(" + __MONTH__ + ")(?:\\s+(\\d{2,4}))?"),
        DATE_SHORT("([0-1]?\\d)[\\.-]([0-3]?\\d)(?:(?:\\s+|\\.|-)(\\d{2,4}))?"),
        // meeting next thursday at 12h, meeting tomorrow at 12:15, meeting today at 15, meeting @ 5 am 
        TIME("(?:(?:eve?ry\\s+)?(?:at|@)\\s+)?(\\d+)h(?:\\s+(\\d+)m)?(?:\\s*(am|pm))?",
             "(?:(?:eve?ry\\s+)?(?:at|@)\\s+)?(\\d+):(\\d+)(?::(\\d+))?(?:\\s*(am|pm))?",
             "(?:eve?ry\\s+)?(?:at|@)\\s*(\\d+)(?:\\s*(am|pm))"),
        YEAR("\\d{2,4}"),
        DONE("done"),/*,
        BEFORE_TASK(),
        NUMBERED_TASK()*/;
        
        final Pattern[] patterns;
        
        private PatternType(String... _patterns) {
            this.patterns = new Pattern[_patterns.length];
            for (int i = 0; i < _patterns.length; i++) {
                final String pattern =  _patterns[i];
                this.patterns[i] = Pattern.compile(((pattern.charAt(0) != '^') ? "\\b" : "") +
                                                   patterns[i] + 
                                                   ((pattern.charAt(pattern.length() - 1) != '$') ? "\\b" : ""), 
                                                   Pattern.CASE_INSENSITIVE);
            }
        }
        
    }
    
    public EnParser() { }    
    
    public TodoItem parse(final String source) {
        
        final TodoItem item = new TodoItem(-1, "");
        
        String itemTitle = source; // TODO: cut out all found match results to get title
        
        // COLLECT TOKENS
        
        final Map<PatternType, List<MatchResult>> tokenData = 
            new LinkedHashMap<PatternType, List<MatchResult>>();
        
        for (PatternType pType: PatternType.values()) {
            tokenData.put(pType, new LinkedList<MatchResult>());
            for (Pattern re: pType.patterns) {
                final Matcher matcher = re.matcher(source);
                while (matcher.find()) {
                    tokenData.get(pType).add(matcher.toMatchResult());                     
                }
                
            }
        }
        
        // PROJECTS
        
        if (tokenData.containsKey(PatternType.PROJECTS_LIST))
            for (MatchResult match: tokenData.get(PatternType.PROJECTS_LIST)) 
                for (int groupId = 0; groupId < match.groupCount(); groupId++)
                    item.addTag(Type.PROJECT, match.group(groupId));
        
        // LOCATIONS
        
        if (tokenData.containsKey(PatternType.LOCATIONS_LIST))
            for (MatchResult match: tokenData.get(PatternType.LOCATIONS_LIST)) 
                for (int groupId = 0; groupId < match.groupCount(); groupId++)
                    item.addTag(Type.LOCATION, match.group(groupId));
        
        // TAGS
        
        if (tokenData.containsKey(PatternType.TAGS_LIST))
            for (MatchResult match: tokenData.get(PatternType.TAGS_LIST)) 
                for (int groupId = 0; groupId < match.groupCount(); groupId++)
                    item.addTag(match.group(groupId));
        
        // DONE
        
        if (tokenData.containsKey(PatternType.DONE)) {
            final Time today = new Time();
            today.setToNow();
            // item.setDone(today); // FIXME: commented   
        }
        
        Set<SchedulingRule> parsedRules = new LinkedHashSet<SchedulingRule>();
        boolean complexRuleFound = false;
        
        // PLUS MINUS
        
        if (tokenData.containsKey(PatternType.PLUS_MINUS)) {
            for (MatchResult match: tokenData.get(PatternType.PLUS_MINUS))
                parsedRules.add(
                        buildPlusMinusRule("+".equals(match.group(0)), Integer.parseInt(match.group(1)), tokenData));
            complexRuleFound = true;
        }
        
        // EVERY PERIOD
        
        if (tokenData.containsKey(PatternType.EVERY_PERIOD)) {
            for (MatchResult match: tokenData.get(PatternType.EVERY_PERIOD)) {
                parsedRules.add(
                        buildRepeatingRule(
                                parseOccurrence(match.group(0)), tokenData));
            }
            complexRuleFound = true;
        }
        
        // EVERY PERIOD AMOUNT
        
        if (tokenData.containsKey(PatternType.EVERY_PERIOD_AMOUNT)) {
            for (MatchResult match: tokenData.get(PatternType.EVERY_PERIOD_AMOUNT)) {
                int parsed = parseNumber(match.group(0));
                parsedRules.add(
                        buildPeriodicalRule(parsed != -1 ? parsed : Integer.parseInt(match.group(0)),
                                            parseOccurrence(match.group(1))));
            }
        }        
        
        // EVERY DATE
        
        if (tokenData.containsKey(PatternType.EVERY_DATE)) {
            for (MatchResult match: tokenData.get(PatternType.EVERY_DATE)) {
                int[] dates = new int[match.groupCount()];
                for (int groupId = 0; groupId < match.groupCount(); groupId++) {
                    dates[groupId] = Integer.parseInt(match.group(groupId));
                }
                parsedRules.add(buildDateRepeatingRule(dates));
            }            
        }        
        
        // EVERY WEEKDAY
        
        if (tokenData.containsKey(PatternType.EVERY_WEEKDAY)) {
            for (MatchResult match: tokenData.get(PatternType.EVERY_WEEKDAY)) {
                int[] weekdays = new int[match.groupCount()];
                for (int groupId = 0; groupId < match.groupCount(); groupId++) {
                    weekdays[groupId] = parseWeekday(match.group(groupId));
                }
                parsedRules.add(buildWeekdayRepeatingRule(weekdays));
            }            
        }

        // EVERY MONTH
        
        if (tokenData.containsKey(PatternType.EVERY_MONTH)) {
            for (MatchResult match: tokenData.get(PatternType.EVERY_MONTH)) {
                int[] months = new int[match.groupCount()];
                for (int groupId = 0; groupId < match.groupCount(); groupId++) {
                    months[groupId] = parseMonth(match.group(groupId));
                }
                parsedRules.add(buildMonthRepeatingRule(months));
            }            
        }
        
        // BACK FWD
        
        if (tokenData.containsKey(PatternType.BACK_FWD)) {
            for (MatchResult match: tokenData.get(PatternType.BACK_FWD)) {
                int parsed = parseNumber(match.group(0));
                parsedRules.add(                      
                    buildBackOrForwardRule(parsed != -1 ? parsed : Integer.parseInt(match.group(0)),
                                           parseOccurrence(match.group(1)),
                                           parseDirection(match.group(2))));
            }            
        }
        
        // AFTER
        
        if (tokenData.containsKey(PatternType.AFTER)) {
            for (MatchResult match: tokenData.get(PatternType.AFTER)) {
                int parsed = parseNumber(match.group(0));                
                parsedRules.add(
                    buildBackOrForwardRule(parsed != -1 ? parsed : Integer.parseInt(match.group(0)),
                                           parseOccurrence(match.group(1)),
                                           1));
            }            
        }
        
        // DURATION

        if (tokenData.containsKey(PatternType.DURATION)) {
            for (MatchResult match: tokenData.get(PatternType.DURATION)) {
                int parsed = parseNumber(match.group(0));
                parsedRules.add(
                    buildDurationRule(parsed != -1 ? parsed : Integer.parseInt(match.group(0)),
                                      parseOccurrence(match.group(1))));
            }
        }
        
        // NEXT

        if (tokenData.containsKey(PatternType.NEXT)) {
            for (MatchResult match: tokenData.get(PatternType.NEXT)) {
                parsedRules.add(
                    buildNextOccurrenceRule(parseOccurrence(match.group(0))));
            }
        }
        
        // FROM and/or TO
        
        final boolean fromFound = tokenData.containsKey(PatternType.FROM);
        final boolean toFound = tokenData.containsKey(PatternType.TO);
        
        if (fromFound || toFound) { 
            
            final int fromCount = tokenData.get(PatternType.FROM).size();
            final int toCount = tokenData.get(PatternType.TO).size();
                
            // if at least one of rules found 
            if ((fromCount > 0) || (toCount > 0)) {
                
                // if there is just one of there rules exists                  
                if ((fromCount == 1) || (toCount == 1)) {
                    
                    final MatchResult fromResult = (fromCount > 0) ? tokenData.get(PatternType.FROM).get(0) : null;
                    final MatchResult toResult = (toCount > 0) ? tokenData.get(PatternType.TO).get(0) : null;
                    
                    parsedRules.add(buildFromToRule(fromResult, toResult, tokenData, source.length()));
                    
                } else { // a lot of such rules exists
                    
                    final int maxCount = (fromCount > toCount) ? fromCount : toCount;
                    
                    for (int i = 0; i < maxCount; i++) {
                        
                        MatchResult fromResult;
                        MatchResult toResult;
                        
                        try { fromResult = tokenData.get(PatternType.FROM).get(i); } 
                        catch (IndexOutOfBoundsException ioobe) { fromResult = null; }
                        
                        try { toResult = tokenData.get(PatternType.TO).get(i); } 
                        catch (IndexOutOfBoundsException ioobe) { toResult = null; }
                        
                        parsedRules.add(buildFromToRule(fromResult, toResult, tokenData, source.length()));
                        
                    }
                    
                }
                
            }
            
            complexRuleFound = true;
        }

        // if no possible complex rules containing simple rules found
        // then there are just simple rules
        if (!complexRuleFound) {
        
            // PERIOD
            
            if (tokenData.containsKey(PatternType.PERIOD)) {
                for (MatchResult match: tokenData.get(PatternType.PERIOD)) {
                    parsedRules.add(buildPeriodRule(parseOccurrence(match.group())));
                }
            }
            
            // YESTERDAY
            
            if (tokenData.containsKey(PatternType.YESTERDAY)) {
                parsedRules.add(buildYesterdayRule());
            }
            
            // TOMORROW
            
            if (tokenData.containsKey(PatternType.TOMORROW)) {
                parsedRules.add(buildTomorrowRule());
            }
            
            // TODAY
            
            if (tokenData.containsKey(PatternType.TODAY)) {
                parsedRules.add(buildTodayRule());
            }
            
            // MONTH WORD
            
            if (tokenData.containsKey(PatternType.MONTH)) {
                for (MatchResult match: tokenData.get(PatternType.MONTH)) {
                    parsedRules.add(buildMonthRule(parseMonth(match.group())));
                }
            }
            
            // WEEKDAY WORD
            
            if (tokenData.containsKey(PatternType.WEEKDAY)) {
                for (MatchResult match: tokenData.get(PatternType.WEEKDAY)) {
                    parsedRules.add(buildWeekdayRule(parseWeekday(match.group())));
                }
            }
            
            // DATE
            
            if (tokenData.containsKey(PatternType.DATE_LONG)) {
                for (MatchResult match: tokenData.get(PatternType.DATE_LONG)) {
                    parsedRules.add(buildDateRule(Integer.parseInt(match.group(0)), 
                                                  parseMonth(match.group(1)),
                                                  match.groupCount() > 2 
                                                       ? Integer.parseInt(match.group(2)) : -1));
                }
            }
            
            if (tokenData.containsKey(PatternType.DATE_SHORT)) {
                for (MatchResult match: tokenData.get(PatternType.DATE_SHORT)) {
                    parsedRules.add(buildDateRule(Integer.parseInt(match.group(1)),
                                                  Integer.parseInt(match.group(0)) - 1,
                                                  match.groupCount() > 2 
                                                       ? Integer.parseInt(match.group(2)) : -1));
                }
            }            
            
            // TIME
            
            if (tokenData.containsKey(PatternType.TIME)) {
                for (MatchResult match: tokenData.get(PatternType.TIME)) {
                    parsedRules.add(buildTimeRule(Integer.parseInt(match.group(0)),
                                                  Integer.parseInt(match.group(1)),
                                                  match.groupCount() > 2
                                                       ? parseAmPm(match.group(2))
                                                       : 0));
                }
            }
            
            // YEAR
            
            if (tokenData.containsKey(PatternType.YEAR)) {
                for (MatchResult match: tokenData.get(PatternType.YEAR)) {
                    parsedRules.add(buildYearRule(Integer.parseInt(match.group())));
                }
            }
            
        }
        
        // TODO: odd or even days
        
        item.addRules(OverlapTest.removeOverlappingRules(parsedRules));
        
        item.changeTitle(itemTitle); // FIXME: remove parsed stuff from title, if it was set in config 
                
        return item;
    }

    protected static int parseNumber(final String number) {
        if ((number == null) || (number.length() == 0)) return -1;
        final String _number = number.toLowerCase();
        if (_number.equals("one")) return 1;
        if (_number.equals("two")) return 2;
        if (_number.equals("three")) return 3;
        if (_number.equals("four")) return 4;
        if (_number.equals("five")) return 5;
        if (_number.equals("six")) return 6;
        if (_number.equals("seven")) return 7;
        if (_number.equals("eight")) return 8;
        if (_number.equals("nine")) return 9;
        if (_number.equals("ten")) return 10;
        return -1;
    }
    
    protected static int parseMonth(final String month) {
        if ((month == null) || (month.length() == 0)) return -1;
        final String _month = month.toLowerCase();
        if (_month.startsWith("jan")) return 0 /*Calendar.JANUARY*/;
        if (_month.startsWith("feb")) return 1 /*Calendar.FEBRUARY*/;
        if (_month.startsWith("mar")) return 2 /*Calendar.MARCH*/;
        if (_month.startsWith("apr")) return 3 /*Calendar.APRIL*/;
        if (_month.startsWith("may")) return 4 /*Calendar.MAY*/;
        if (_month.startsWith("jun")) return 5 /*Calendar.JUNE*/;
        if (_month.startsWith("jul")) return 6 /*Calendar.JULY*/;
        if (_month.startsWith("aug")) return 7 /*Calendar.AUGUST*/;
        if (_month.startsWith("sep")) return 8 /*Calendar.SEPTEMBER*/;
        if (_month.startsWith("oct")) return 9 /*Calendar.OCTOBER*/;
        if (_month.startsWith("nov")) return 10 /*Calendar.NOVEMBER*/;
        if (_month.startsWith("dec")) return 11 /*Calendar.DECEMBER*/;
        return -1;
    }
    
    protected static int parseWeekday(final String weekday) {
        if ((weekday == null) || (weekday.length() == 0)) return -1;
        final String _weekday = weekday.toLowerCase();
        if (_weekday.startsWith("mon")) return Time.MONDAY;
        if (_weekday.startsWith("tue")) return Time.TUESDAY;
        if (_weekday.startsWith("wed")) return Time.WEDNESDAY;
        if (_weekday.startsWith("thu")) return Time.THURSDAY;
        if (_weekday.startsWith("fri")) return Time.FRIDAY;
        if (_weekday.startsWith("sat")) return Time.SATURDAY;
        if (_weekday.startsWith("sun")) return Time.SUNDAY;
        return -1;        
    }   
    
    protected static Occurrence parseOccurrence(final String occurence) {
        if ((occurence == null) || (occurence.length() == 0)) return null;
        final String _occurence = occurence.toLowerCase();
        if (_occurence.startsWith("day")) return Occurrence.DAY;
        if (_occurence.startsWith("week")) return Occurrence.WEEK;
        if (_occurence.startsWith("month") || 
            _occurence.startsWith("mnth")) return Occurrence.MONTH;
        if (_occurence.startsWith("year") || 
            _occurence.startsWith("yr")) return Occurrence.YEAR;
        if (_occurence.startsWith("hour") || 
            _occurence.startsWith("hr") || 
            _occurence.equals("h")) return Occurrence.HOUR;
        if (_occurence.startsWith("minute") || 
            _occurence.startsWith("mnt")) return Occurrence.MINUTE;
        return null;
    }
    
    protected static int parseDirection(final String direction) {
        // (ba?ck|before|b4|ago|fwd|forth)
        if ((direction == null) || (direction.length() == 0)) return 0;
        final String _direction = direction.toLowerCase();
        if (_direction.equals("fwd") ||
            _direction.equals("forward") ||
            _direction.equals("forth")) return 1;        
        if (_direction.equals("ago") ||
            _direction.equals("back") ||
            _direction.equals("bck") ||
            _direction.equals("before") ||            
            _direction.equals("b4")) return -1;
        return 0;
    }
    
    protected static int parseAmPm(final String amPm) {
        if ((amPm == null) || (amPm.length() == 0)) return 0;
        if (amPm.equalsIgnoreCase("am")) return 0;
        if (amPm.equalsIgnoreCase("pm")) return 1;
        return -1;
    }
    
    protected static Time extractLongDate(MatchResult match) {
        final Time result = new Time(); result.setToNow();
        result.monthDay = Integer.parseInt(match.group(0));
        result.month = parseMonth(match.group(1));
        result.year = match.groupCount() > 2 
                      ? Integer.parseInt(match.group(2)) : -1;
        return result;
    }
    
    protected static Time extractShortDate(MatchResult match) {
        final Time result = new Time(); result.setToNow();
        result.monthDay = Integer.parseInt(match.group(1));
        result.month = Integer.parseInt(match.group(0)) - 1;
        result.year = match.groupCount() > 2 
                      ? Integer.parseInt(match.group(2)) : -1;
        return result;
    }
    
    protected static Time extractTime(MatchResult match) {
        final Time result = new Time(); result.setToNow();
        int amPm = (match.groupCount() > 2)
                   ? parseAmPm(match.group(2))
                   : 0;
        result.hour = Integer.parseInt(match.group(0)) + ((amPm != -1) ? (amPm * 12) : 0);
        result.minute = Integer.parseInt(match.group(1));
        return result;
    }
    
    protected interface RuleBuilder {
        public SchedulingRule build(MatchResult from, MatchResult to);
    }
    
    protected static SchedulingRule getRuleForPattern(
                                          final int fromPos,
                                          final int toPos,
                                          final PatternType pattern, 
                                          final Map<PatternType, List<MatchResult>> tokenData,
                                          final int srcLen,
                                          final RuleBuilder builder) {
        
        if (tokenData.containsKey(pattern)) {
            
            MatchResult nearestFrom = null;
            MatchResult nearestTo = null;
            
            int foundFrom = srcLen;
            int foundTo = srcLen;            
            
            final List<MatchResult> matches = tokenData.get(pattern);
            for (MatchResult match: matches) {
                if ((fromPos >= 0) &&
                    (match.start() < foundFrom) &&
                    (match.start() >= fromPos)) {
                    foundFrom = match.start();
                    nearestFrom = match;
                }
                if ((toPos >= 0) &&
                    (match.start() < foundTo) &&
                    (match.start() >= toPos)) {
                    foundTo = match.start();
                    nearestTo = match;
                }
            }
            
            return builder.build((fromPos > 0) ? nearestFrom : null, 
                                 (toPos > 0) ? nearestTo : null);
        }
        
        return null;
    }
    
    protected static SchedulingRule buildFromToRule(MatchResult fromResult,
            MatchResult toResult, Map<PatternType, List<MatchResult>> tokenData, int srcLen) {
        
        final int fromPos = (fromResult != null) ? fromResult.start() : -1;
        final int toPos = (toResult != null) ? toResult.start() : -1;
        
        SchedulingRule rule = null;
        
        // FIXME: cache RuleBuilders
        
        rule = getRuleForPattern(fromPos, toPos, PatternType.DATE_LONG,
                                 tokenData, srcLen, new RuleBuilder() {
                                     @Override public SchedulingRule build(MatchResult from, MatchResult to) {
                                         final Time fromTime = (from != null) ? extractLongDate(from) : null;
                                         final Time toTime = (to != null) ? extractLongDate(to) : null;
                                         return new OccursSingleTime().setRun(fromTime, toTime);
                                      }
                                 });
        
        if (rule != null) return rule;
        
        rule = getRuleForPattern(fromPos, toPos, PatternType.DATE_SHORT,
                  tokenData, srcLen, new RuleBuilder() {
                            @Override public SchedulingRule build(MatchResult from, MatchResult to) {
                                final Time fromTime = (from != null) ? extractLongDate(from) : null;
                                final Time toTime = (to != null) ? extractLongDate(to) : null;
                                return new OccursSingleTime().setRun(fromTime, toTime);
                             }
                      });

        if (rule != null) return rule;
        
        rule = getRuleForPattern(fromPos, toPos, PatternType.DATE_SHORT,
                tokenData, srcLen, new RuleBuilder() {
                          @Override public SchedulingRule build(MatchResult from, MatchResult to) {
                              final Time fromTime = (from != null) ? extractShortDate(from) : null;
                              final Time toTime = (to != null) ? extractShortDate(to) : null;
                              return new OccursSingleTime().setRun(fromTime, toTime);
                           }
                    });

        if (rule != null) return rule;
        
        rule = getRuleForPattern(fromPos, toPos, PatternType.TIME,
                tokenData, srcLen, new RuleBuilder() {
                          @Override public SchedulingRule build(MatchResult from, MatchResult to) {
                              final Time fromTime = (from != null) ? extractTime(from) : null;
                              final Time toTime = (to != null) ? extractTime(to) : null;
                              return new OccursSingleTime().setRun(fromTime, toTime);
                           }
                    });

        if (rule != null) return rule;
        
        rule = getRuleForPattern(fromPos, toPos, PatternType.YEAR,
                tokenData, srcLen, new RuleBuilder() {
                          @Override public SchedulingRule build(MatchResult from, MatchResult to) {
                              Time fromTime = null;
                              if (from != null) {
                                  fromTime = new Time();
                                  fromTime.setToNow();
                                  fromTime.year = Integer.parseInt(from.group(0));                                  
                              }
                              Time toTime = null;
                              if (to != null) {
                                  toTime = new Time();
                                  toTime.setToNow();
                                  toTime.year = Integer.parseInt(to.group(0));                                  
                              }
                              return new OccursSingleTime().setRun(fromTime, toTime);
                           }
                    });

        if (rule != null) return rule;        
        
        // FIXME: merge TODAY/TOMORROW/YESTERDAY rules into one, and then implement
        
        // FIXME: implement MONTH, WEEKDAY, NEXT
        
        // FIXME: check if both time data and date data are present
        
        return null;
    }
    
    
    
    protected static RepeatingRule buildRepeatingRule(Occurrence occurrence,
            Map<PatternType, List<MatchResult>> tokenData) {
        final Time today = new Time();
        today.setToNow();
        switch (occurrence) {
            case YEAR: {
                    final RepeatsDatesInYear rule = new RepeatsDatesInYear();
                    if (tokenData.containsKey(PatternType.DATE_LONG)) {
                        for (final MatchResult match: tokenData.get(PatternType.DATE_LONG)) {
                            int month = parseMonth(match.group(1));
                            if (month == -1) month = Integer.parseInt(match.group(1)) - 1;
                            rule.addDate(Integer.valueOf(match.group(0)), month);
                        }
                    } if (tokenData.containsKey(PatternType.DATE_SHORT)) {
                        for (final MatchResult match: tokenData.get(PatternType.DATE_SHORT)) {
                            int month = Integer.parseInt(match.group(0)) - 1;
                            rule.addDate(Integer.valueOf(match.group(1)), month);
                        }
                    } else rule.addDate(today.monthDay, today.month);
                    return rule;
                }
            case MONTH: 
                return new RepeatsDaysInMonth().addDate(today.monthDay);                  
            case WEEK:
                return new RepeatsByWeekdays().addWeekday(today.weekDay);
            case DAY: {
                    final RepeatsEveryDay rule = new RepeatsEveryDay();
                    if (tokenData.containsKey(PatternType.TIME)) {
                        for (final MatchResult match: tokenData.get(PatternType.TIME)) {
                            if (match.groupCount() > 1) {
                                int hour = Integer.parseInt(match.group(0));
                                int minute = Integer.parseInt(match.group(1));
                                int amPm = match.groupCount() > 2
                                           ? parseAmPm(match.group(2)) : 0;                            
                                rule.addTime(hour, minute, amPm);
                            }
                        }
                    } else rule.addTime(today.hour, today.minute);
                    return rule;
                }
            case HOUR:
                return new RepeatsByMinutes().addMinute(today.minute);
            case MINUTE: return null;
        }
        return null;
    }    
    
    protected static SchedulingRule buildPlusMinusRule(boolean forward, int amount,
            Map<PatternType, List<MatchResult>> tokenData) {
        final Time time = new Time();
        time.setToNow();
        
        int rollAmount = forward ? amount : (amount * -1);
        if (tokenData.containsKey(PatternType.YESTERDAY)) time.monthDay += (rollAmount - 1);
        else if (tokenData.containsKey(PatternType.TOMORROW)) time.monthDay += (rollAmount + 1);
        else time.monthDay += rollAmount;
        time.normalize(true);
        return new OccursSingleTime().setTime(time);
    }    
    
    protected static RepeatingRule buildPeriodicalRule(int amount, Occurrence occurrence) {
        return null; // FIXME: implement
    }
    
    protected static RepeatingRule buildDateRepeatingRule(int[] dates) {
        final Time today = new Time(); today.setToNow();
        return new RepeatsDaysInMonth().addDates(dates);
    }    

    protected static RepeatingRule buildWeekdayRepeatingRule(int[] weekdays) {
        return new RepeatsByWeekdays().addWeekdays(weekdays);
    }
    
    protected static RepeatingRule buildMonthRepeatingRule(int[] months) {
        return new RepeatsMonthsInYear().addMonths(months);
    }
        
    protected static SchedulingRule buildBackOrForwardRule(int amount,
                             Occurrence period, int direction) {
        return null; // FIXME: implement
    }
    
    protected static SchedulingRule buildDurationRule(int amount, Occurrence occurence) {
        return null; // FIXME: implement
    }
    
    protected static SchedulingRule buildNextOccurrenceRule(Occurrence occurence) {
        return null; // FIXME: implement
    }
    
    protected static SchedulingRule buildPeriodRule(Occurrence occurence) {
        return null; // FIXME: implement
    }
    
    protected static SchedulingRule buildTimeRule(int hour, int minute, int amPm) {
        final Time time = new Time(); time.setToNow();
        time.hour = (amPm != -1) ? (hour + (amPm * 12)) : hour;
        time.minute = minute;
        return new OccursSingleTime().setTime(time);
    }

    protected static SchedulingRule buildDateRule(int day, int month, int year) {
        return new OccursSingleTime().setDate(day, month, year);
    }

    protected static SchedulingRule buildWeekdayRule(int weekday) {
        final Time today = new Time(); today.setToNow();
        today.monthDay += (weekday - today.weekDay);
        today.normalize(true);
        return new OccursSingleTime().setDate(today.monthDay, today.month, today.year);
    }

    protected static SchedulingRule buildMonthRule(int month) {
        final Time today = new Time(); today.setToNow();        
        today.month = month; today.normalize(true);
        return new OccursSingleTime().setDate(today.monthDay, today.month, today.year);
    }
    
    protected static SchedulingRule buildYearRule(int year) {
        final Time today = new Time(); today.setToNow();
        return new OccursSingleTime().setDate(today.monthDay, today.month, year);    
    }

    protected static SchedulingRule buildTodayRule() {
        final Time today = new Time(); today.setToNow();
        return new OccursSingleTime().setDate(today.monthDay, today.month, today.year);
    }

    protected static SchedulingRule buildTomorrowRule() {
        final Time today = new Time(); today.setToNow();
        today.monthDay++; today.normalize(true);
        return new OccursSingleTime().setDate(today.monthDay, today.month, today.year);
    }

    protected static SchedulingRule buildYesterdayRule() {
        final Time today = new Time(); today.setToNow();
        today.monthDay--; today.normalize(true);
        return new OccursSingleTime().setDate(today.monthDay, today.month, today.year);
    }

}
